﻿using System;
using System.Text;
using System.Collections;
using System.Globalization;
using System.Collections.Generic;

namespace Common.ExpressionParsing
{
    public class EvaluationVisitor : LogicalExpressionVisitor
    {
        #region Properties

        // It is needed for tranfer parameters beetwen functions
        public Hashtable Parameters { get; set; }

        // It is needed for inserting external parameters
        public Dictionary<String, String> ExternalParams { get; set; }

        private NumberFormatInfo NumberFormatInfo { get; set; }

        #endregion Properties


        #region Constructors

        public EvaluationVisitor()
        {
            NumberFormatInfo = new NumberFormatInfo();
            NumberFormatInfo.NumberDecimalSeparator = ".";
        }

        #endregion Constructors


        #region Methods

        public override Object Evaluate(LogicalExpression expression)
        {
            expression.Accept(this);
            return Result;
        }

        #region Stub for LogicalExpression Visit

        public override void Visit(LogicalExpression expression)
        {
            throw new NotImplementedException();
        }

        #endregion Stub for LogicalExpression Visit


        #region BinaryExpresssion Visit

        public override void Visit(BinaryExpresssion expression)
        {
            // Evaluates the left expression and saves the value
            expression.LeftExpression.Accept(this);
            object left = Result;

            // Evaluates the right expression and saves the value
            expression.RightExpression.Accept(this);
            object right = Result;

            switch (expression.Type)
            {
                case BinaryExpressionType.And:
                    Result = Convert.ToBoolean(left) && Convert.ToBoolean(right);
                    break;

                case BinaryExpressionType.Or:
                    Result = Convert.ToBoolean(left) || Convert.ToBoolean(right);
                    break;

                case BinaryExpressionType.Div:
                    Result = Convert.ToDecimal(left) / Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Equal:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                        Convert.ChangeType(right, left.GetType())) == 0;
                    break;

                case BinaryExpressionType.Greater:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                        Convert.ChangeType(right, left.GetType())) > 0;
                    break;

                case BinaryExpressionType.GreaterOrEqual:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                        Convert.ChangeType(right, left.GetType())) >= 0;
                    break;

                case BinaryExpressionType.Lesser:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                        Convert.ChangeType(right, left.GetType())) < 0;
                    break;

                case BinaryExpressionType.LesserOrEqual:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                        Convert.ChangeType(right, left.GetType())) <= 0;
                    break;

                case BinaryExpressionType.Minus:
                    Result = Convert.ToDecimal(left) - Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Modulo:
                    Result = Convert.ToDecimal(left) % Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.NotEqual:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                        Convert.ChangeType(right, left.GetType())) != 0;
                    break;

                case BinaryExpressionType.Plus:
                    Result = Convert.ToDecimal(left) + Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Times:
                    Result = Convert.ToDecimal(left) * Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Pow:
                    Result = Math.Pow(Convert.ToDouble(left), Convert.ToDouble(right));
                    break;

            }
        }

        #endregion BinaryExpresssion Visit


        #region UnaryExpression Visit

        public override void Visit(UnaryExpression expression)
        {
            // Recursively evaluates the underlying expression
            expression.Expression.Accept(this);

            switch (expression.Type)
            {
                case UnaryExpressionType.Not:
                    Result = !Convert.ToBoolean(Result);
                    break;

                case UnaryExpressionType.Negate:
                    Result = -Convert.ToDecimal(Result);
                    break;
            }
        }

        #endregion UnaryExpression Visit


        #region Function Visit

        public override void Visit(Function function)
        {
            // Evaluates all parameters
            object[] parameters = new object[function.Expressions.Length];
            for (int i = 0; i < parameters.Length; i++)
                parameters[i] = Evaluate(function.Expressions[i]);

            FunctionArgs args = new FunctionArgs();
            args.Parameters = parameters;

            // Calls external implementation
            OnEvaluateFunction(function.Identifier, args);

            // If an external implementation was found get the result back
            if (args.Result != null)
            {
                Result = args.Result;
                return;
            }

            String functionIdentifier = function.Identifier.ToLower();

            switch (functionIdentifier)
            {
                #region Abs
                case "abs":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Abs() takes exactly 1 argument");

                    Result = Math.Abs(Convert.ToDecimal(
                        Evaluate(function.Expressions[0]))
                        );

                    break;

                #endregion

                #region Acos
                case "acos":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Acos() takes exactly 1 argument");

                    Result = Math.Acos(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Asin
                case "asin":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Asin() takes exactly 1 argument");

                    Result = Math.Asin(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Atan
                case "atan":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Atan() takes exactly 1 argument");

                    Result = Math.Atan(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Ceiling
                case "ceiling":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Ceiling() takes exactly 1 argument");

                    Result = Math.Ceiling(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Cos

                case "cos":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Cos() takes exactly 1 argument");

                    Result = Math.Cos(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Exp
                case "exp":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Exp() takes exactly 1 argument");

                    Result = Math.Exp(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Floor
                case "floor":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Floor() takes exactly 1 argument");

                    Result = Math.Floor(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region IEEERemainder
                case "ieeeremainder":

                    if (function.Expressions.Length != 2)
                        throw new ArgumentException("IEEERemainder() takes exactly 2 arguments");

                    Result = Math.IEEERemainder(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToDouble(Evaluate(function.Expressions[1])));

                    break;

                #endregion

                #region Log
                case "log":

                    if (function.Expressions.Length != 2)
                        throw new ArgumentException("Log() takes exactly 2 arguments");

                    Result = Math.Log(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToDouble(Evaluate(function.Expressions[1])));

                    break;

                #endregion

                #region Log10
                case "log10":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Log10() takes exactly 1 argument");

                    Result = Math.Log10(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Pow
                case "pow":

                    if (function.Expressions.Length != 2)
                        throw new ArgumentException("Pow() takes exactly 2 arguments");

                    Result = Math.Pow(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToDouble(Evaluate(function.Expressions[1])));

                    break;

                #endregion

                #region Round
                case "round":

                    if (function.Expressions.Length != 2)
                        throw new ArgumentException("Round() takes exactly 2 arguments");

                    Result = Math.Round(Convert.ToDouble(Evaluate(function.Expressions[0])), Convert.ToInt16(Evaluate(function.Expressions[1])));

                    break;

                #endregion

                #region Sign
                case "sign":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Sign() takes exactly 1 argument");

                    Result = Math.Sign(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Sin
                case "sin":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Sin() takes exactly 1 argument");

                    Result = Math.Sin(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Sqrt
                case "sqrt":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Sqrt() takes exactly 1 argument");

                    Result = Math.Sqrt(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Tan
                case "tan":

                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Tan() takes exactly 1 argument");

                    Result = Math.Tan(Convert.ToDouble(Evaluate(function.Expressions[0])));

                    break;

                #endregion

                #region Max
                case "max":

                    if (function.Expressions.Length != 2)
                        throw new ArgumentException("Max() takes exactly 2 arguments");

                    decimal maxleft = Convert.ToDecimal(Evaluate(function.Expressions[0]));
                    decimal maxright = Convert.ToDecimal(Evaluate(function.Expressions[1]));

                    Result = Math.Max(maxleft, maxright);
                    break;

                #endregion

                #region Min
                case "min":

                    if (function.Expressions.Length != 2)
                        throw new ArgumentException("Min() takes exactly 2 arguments");

                    decimal minleft = Convert.ToDecimal(Evaluate(function.Expressions[0]));
                    decimal minright = Convert.ToDecimal(Evaluate(function.Expressions[1]));

                    Result = Math.Min(minleft, minright);
                    break;

                #endregion

                #region if
                case "if":

                    if (function.Expressions.Length != 3)
                        throw new ArgumentException("if() takes exactly 3 arguments");

                    bool cond = Convert.ToBoolean(Evaluate(function.Expressions[0]));
                    object then = Evaluate(function.Expressions[1]);
                    object els = Evaluate(function.Expressions[2]);

                    Result = cond ? then : els;
                    break;

                #endregion

                #region DimaFun
                case "dimafun":

                    Result = 1;
                    break;

                #endregion

                #region Pi
                case "pi":
                    if (function.Expressions.Length > 0)
                        throw new ArgumentException("Pi() takes exactly 0 arguments");

                    Result = Math.PI;
                    break;
                #endregion

                #region E
                case "e":
                    if (function.Expressions.Length > 0)
                        throw new ArgumentException("E() takes exactly 0 arguments");

                    Result = Math.E;
                    break;
                #endregion

                #region Param
                case "param":
                    if (function.Expressions.Length != 1)
                        throw new ArgumentException("Param() takes exactly 1 argument");

                    String name = Convert.ToString(Evaluate(function.Expressions[0]));
                    String expression = ExternalParams[name];

                    Expression innerExpr = new Expression(expression);
                    innerExpr.ExternalParams = ExternalParams;
                    Object paramValue = innerExpr.Evaluate();

                    Result = paramValue;

                    break;
                #endregion

                default:
                    if (ExtensionManager != null && ExtensionManager.IsExtensionExist(functionIdentifier))
                    {
                        IFunctionVisitorExtension extFunction =
                            ExtensionManager.GetFunctionVisitor(functionIdentifier);
                        Result = extFunction.Execute(functionIdentifier, function.Expressions, this);
                    }
                    else
                        throw new ArgumentException("Function not found",
                            function.Identifier);
                    break;
            }
        }

        #endregion Function Visit


        #region Parameter Visit

        public override void Visit(Parameter parameter)
        {
            if (Parameters.Contains(parameter.Name))
            {
                // The parameter is defined in the hashtable
                if (Parameters[parameter.Name] is Expression)
                {
                    // The parameter is itself another Expression
                    Expression expression = (Expression)Parameters[parameter.Name];
                    expression.Parameters = Parameters;
                    expression.EvaluateFunction += EvaluateFunction;
                    expression.EvaluateParameter += EvaluateParameter;

                    Result = ((Expression)Parameters[parameter.Name]).Evaluate();
                }
                else
                    Result = Parameters[parameter.Name];
            }
            else
            {
                // The parameter should be defined in a call back method
                ParameterArgs args = new ParameterArgs();

                // Calls external implementation
                OnEvaluateParameter(parameter.Name, args);

                if (args.Result == null)
                    throw new ArgumentException("Parameter was not defined", parameter.Name);

                Result = args.Result;
            }
        }

        #endregion Parameter Visit


        #region Value Visit

        public override void Visit(Value expression)
        {
            switch (expression.Type)
            {
                case ValueType.Boolean:
                    Result = bool.Parse(expression.Text);
                    break;

                case ValueType.DateTime:
                    Result = DateTime.Parse(expression.Text.Substring(1, expression.Text.Length - 2));
                    break;

                case ValueType.Float:
                    Result = decimal.Parse(expression.Text, NumberFormatInfo);
                    break;

                case ValueType.Integer:
                    Result = int.Parse(expression.Text);
                    break;

                case ValueType.String:
                    Result = expression.Text;
                    break;
            }
        }

        #endregion Value Visit

        #endregion Methods


        #region Events

        public event EvaluateFunctionHandler EvaluateFunction;
        public virtual void OnEvaluateFunction(string name, FunctionArgs args)
        {
            EvaluateFunctionHandler evaluateFunction = EvaluateFunction;
            if (evaluateFunction != null)
                evaluateFunction(name, args);
        }

        public event EvaluateParameterHandler EvaluateParameter;
        public virtual void OnEvaluateParameter(string name, ParameterArgs args)
        {
            EvaluateParameterHandler evaluateParameter = EvaluateParameter;
            if (evaluateParameter != null)
                evaluateParameter(name, args);
        }

        #endregion Events

    }
}
